﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Proyecta.Data.Helpers
{
    /// <summary>
    /// Permite crear una consulta de selección SQL.
    /// </summary>
    /// <remarks>En esta clase se empleo el patron de diseno Decorator,
    /// dado que se empleo la clase Query como base para la creacion de
    /// una consulta tipo SELECT.</remarks>
    public class SelectQuery : Query
    {
        private string[] Columnas { get; set; }
        private string Tabla { get; set; }
        private string Alias { get; set; }
        private List<QueryJoin> Joins { get; set; }
        private QueryConditions CondicionesWhere { get; set; }
        private List<QueryOrderedColumn> ColumnasOrdenar { get; set; }
        private string[] ColumnasGrupo { get; set; }
        private QueryConditions CondicionesHaving { get; set; }

        /// <summary>
        /// Permite crear una consulta de selección SQL.
        /// </summary>
        public SelectQuery()
        {
            this.Joins = new List<QueryJoin>();
            this.CondicionesWhere = new QueryConditions(this);
            this.ColumnasOrdenar = new List<QueryOrderedColumn>();
            this.CondicionesHaving = new QueryConditions(this);
        }

        /// <summary>
        /// Selecciona las columnas de una tabla. Equivalente a SELECT en SQL.
        /// </summary>
        /// <param name="columnas">Lista de columnas a traer de la base de datos.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery Select(params string[] columnas)
        {
            this.Columnas = columnas;
            return this;
        }

        /// <summary>
        /// Indica cual será la tabla que será el origen de datos de la consulta.
        /// </summary>
        /// <param name="tabla">Nombre de la tabla que será el origen de datos.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery From(string tabla)
        {
            this.Tabla = tabla;
            return this;
        }

        /// <summary>
        /// Indica cual será la tabla que será el origen de datos de la consulta, especificando
        /// un alias para la misma.
        /// </summary>
        /// <param name="tabla">Nombre de la tabla que será el origen de datos.</param>
        /// <param name="alias">Alias para la tabla</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery From(string tabla, string alias)
        {
            this.Tabla = tabla;
            this.Alias = alias;
            return this;
        }

        /// <summary>
        /// Realiza una union de tablas de tipo INNER JOIN.
        /// </summary>
        /// <param name="tabla">Tabla a unir.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQueryJoin InnerJoin(string tabla)
        {
            return this.Join(JoinType.Inner, tabla);
        }

        /// <summary>
        /// Realiza una union de tablas de tipo INNER JOIN, especificando
        /// un alias para la tabla.
        /// </summary>
        /// <param name="tabla">Tabla a unir.</param>
        /// <param name="alias">Alias para la tabla</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQueryJoin InnerJoin(string tabla, string alias)
        {
            return this.Join(JoinType.Inner, tabla, alias);
        }

        /// <summary>
        /// Realiza una union de tablas de tipo LEFT JOIN.
        /// </summary>
        /// <param name="tabla">Tabla a unir.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQueryJoin LeftJoin(string tabla)
        {
            return this.Join(JoinType.Left, tabla);
        }

        /// <summary>
        /// Realiza una union de tablas de tipo LEFT JOIN, especificando
        /// un alias para la tabla.
        /// </summary>
        /// <param name="tabla">Tabla a unir.</param>
        /// <param name="alias">Alias para la tabla</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQueryJoin LeftJoin(string tabla, string alias)
        {
            return this.Join(JoinType.Left, tabla, alias);
        }

        /// <summary>
        /// Realiza una union de tablas de tipo RIGHT JOIN.
        /// </summary>
        /// <param name="tabla">Tabla a unir.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQueryJoin RightJoin(string tabla)
        {
            return this.Join(JoinType.Right, tabla);
        }

        /// <summary>
        /// Realiza una union de tablas de tipo RIGHT JOIN, especificando
        /// un alias para la tabla.
        /// </summary>
        /// <param name="tabla">Tabla a unir.</param>
        /// <param name="alias">Alias para la tabla</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQueryJoin RightJoin(string tabla, string alias)
        {
            return this.Join(JoinType.Right, tabla, alias);
        }

        private SelectQueryJoin Join(JoinType tipo, string tabla)
        {
            return this.Join(tipo, tabla, "");
        }

        private SelectQueryJoin Join(JoinType tipo, string tabla, string alias)
        {
            SelectQueryJoin join = new SelectQueryJoin(this);
            join.Tipo = tipo;
            join.Tabla = tabla;
            join.Alias = alias;
            this.Joins.Add(join);
            return join;
        }

        /// <summary>
        /// Agrega una condicion de filtrado WHERE en la consulta SQL.
        /// </summary>
        /// <param name="exp1">Nombre del campo a condicionar</param>
        /// <param name="comparacion">Tipo de comparacion.</param>
        /// <param name="exp2">Valor a comparar.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery Where(string exp1, CompareOperator comparacion, string exp2)
        {
            return (SelectQuery)this.CondicionesWhere.Add(exp1, comparacion, exp2);
        }

        /// <summary>
        /// Agrega una condicion de filtrado WHERE en la consulta SQL, con la opcion
        /// de agregar una negacion NOT.
        /// </summary>
        /// <param name="negar">True para usar negacion con NOT</param>
        /// <param name="exp1">Nombre del campo a condicionar</param>
        /// <param name="comparacion">Tipo de comparacion.</param>
        /// <param name="exp2">Valor a comparar.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery Where(bool negar, string exp1, CompareOperator comparacion, string exp2)
        {
            return (SelectQuery)this.CondicionesWhere.Add(LogicOperator.And, negar, exp1, comparacion, exp2);
        }

        /// <summary>
        /// Agrega un bloque de condiciones de filtrado WHERE en la consulta SQL.
        /// </summary>
        /// <param name="condiciones">Lista de objetos QueryCondition con los datos de las condiciones. Utilizar
        /// los metodos estaticos SelectQuery.Condition, SelectQuery.And o SelectQuery.Or para crearlos.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery Where(params QueryCondition[] condiciones)
        {
            return (SelectQuery)this.CondicionesWhere.Add(condiciones);
        }

        /// <summary>
        /// Agrega la lista de campos a utilizar para agrupar los datos.
        /// </summary>
        /// <param name="columnas">Lista de columnas para agrupar</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery GroupBy(params string[] columnas)
        {
            this.ColumnasGrupo = columnas;
            return this;
        }

        /// <summary>
        /// Agrega una columna a la seccion ORDER BY de la consulta SQL.
        /// </summary>
        /// <param name="columna">Nombre de la columna para realizar la ordenacion.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery OrderBy(string columna)
        {
            this.OrderBy(columna, OrderType.Ascendente);
            return this;
        }

        /// <summary>
        /// Agrega una columna a la seccion ORDER BY de la consulta SQL.
        /// </summary>
        /// <param name="columna">Nombre de la columna para realizar la ordenacion.</param>
        /// <param name="tipo">Modo de ordenacion a emplear.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery OrderBy(string columna, OrderType tipo)
        {
            this.ColumnasOrdenar.Add(new QueryOrderedColumn(columna, tipo));
            return this;
        }

        /// <summary>
        /// Agrega una lista de columnas a la seccion ORDER BY de la consulta SQL.
        /// </summary>
        /// <param name="columnas">Lista de objetos de tipo QueryOrderedColumn con los datos de las
        /// columnas a ordenar. Utilizar el metodo estatico SelectQuery.OrderColumn para crear cada elemento.</param>
        /// <returns>Instancia del objeto SelectQuery actual.</returns>
        public SelectQuery OrderBy(params QueryOrderedColumn[] columnas)
        {
            foreach (QueryOrderedColumn col in columnas)
                this.ColumnasOrdenar.Add(col);
            return this;
        }

        public SelectQuery Having(params QueryCondition[] condiciones)
        {
            return (SelectQuery)this.CondicionesHaving.Add(condiciones);
        }

        public SelectQuery Having(string exp1, CompareOperator comparacion, string exp2)
        {
            return (SelectQuery)this.CondicionesHaving.Add(exp1, comparacion, exp2);
        }

        public SelectQuery Having(bool negar, string exp1, CompareOperator comparacion, string exp2)
        {
            return (SelectQuery)this.CondicionesHaving.Add(LogicOperator.And, negar, exp1, comparacion, exp2);
        }

        public SelectQuery Having(LogicOperator operador, bool negar, string exp1, CompareOperator comparacion, string exp2)
        {
            QueryCondition cond = new QueryCondition();
            cond.Operador = operador;
            cond.Negar = negar;
            cond.Expresion1 = exp1;
            cond.Comparacion = comparacion;
            cond.Expresion2 = exp2;
            return (SelectQuery)this.CondicionesHaving.Add(cond);
        }

        /// <summary>
        /// Devuelve la sentencia SQL.
        /// </summary>
        public override string ToString()
        {
            StringBuilder sql = new StringBuilder();
            sql.AppendLine("SELECT " + string.Join(", ", this.Columnas));
            sql.Append("FROM " + this.Tabla);
            if (!string.IsNullOrEmpty(this.Alias)) sql.Append(" AS " + this.Alias);
            sql.AppendLine();

            if (this.Joins.Count > 0)
            {
                foreach (QueryJoin j in this.Joins)
                    j.Generate(sql);
            }

            if (this.CondicionesWhere.Count > 0)
            {
                sql.Append("WHERE ");
                this.CondicionesWhere.Generate(sql);
                sql.AppendLine();
            }

            if (this.ColumnasGrupo.Length > 0)
            {
                sql.Append("GROUP BY ");
                sql.AppendLine(string.Join(", ", this.ColumnasGrupo));
            }

            if (this.CondicionesHaving.Count > 0)
            {
                sql.Append("HAVING ");
                this.CondicionesHaving.Generate(sql);
                sql.AppendLine();
            }

            if (this.ColumnasOrdenar.Count > 0)
            {
                sql.Append("ORDER BY ");
                int i = 0;
                foreach (QueryOrderedColumn col in this.ColumnasOrdenar)
                {
                    if (i++ > 0) sql.Append(", ");
                    col.Generate(sql);
                }
                sql.AppendLine();
            }

            return sql.ToString();
        }
    }
}